define
(
    ["Data/Document",
     "Messages/JoinDocumentRequest",
     "Messages/JoinDocumentResponse", 
     "Messages/UserStatusChangedNotification",
     "Messages/DocumentChangeRequest",
     "Messages/DocumentChangeResponse",
     "Messages/ElementCreateAction",
     "Messages/ElementDeleteAction",
     "Messages/ElementModifyAction",
     "Messages/FrameCreateAction",
     "Messages/FrameDeleteAction",
     "Messages/FrameReorderAction",
     "Messages/TaskCreateAction",
     "Messages/AddSoundAction",
     "Messages/DocumentChangedNotification",
     "Messages/ElementCreateResult",
     "Messages/FrameCreateResult",
     "Messages/TaskCreateResult",
     "Strings",
     "Common/Validate"],
    function(Document,
                 JoinDocumentRequest,
                 JoinDocumentResponse,
                 UserStatusChangedNotification,
                 DocumentChangeRequest,
                 DocumentChangeResponse,
                 ElementCreateAction,
                 ElementDeleteAction,
                 ElementModifyAction,
                 FrameCreateAction,
                 FrameDeleteAction,
                 FrameReorderAction,
                 TaskCreateAction,
                 AddSoundAction,
                 DocumentChangedNotification,
                 ElementCreateResult,
                 FrameCreateResult,
                 TaskCreateResult,
                 Strings,
                 Validate)
    {
        var singleton = function(io)
        {
            var _availableId = 0;
            var _documents = {};
            var _newDocuments = {};
            var User = require("Data/User");

            if (!io)
            {
                throw "Invalid io";
            }

            this.handleSocketEvents = function(socket)
            {
                bindEvent(JoinDocumentRequest.id, socket, joinDocument);
                bindEvent(DocumentChangeRequest.id, socket, documentChanged);
                bindEvent('disconnect', socket, socketDisconnected);
            };

            this.createDocument = function(sessionId)
            {
                _newDocuments[_availableId] = sessionId;
                return _availableId++;
            };

            function joinDocument(clientSocket, msg)
            {
                // No document exists
                var documentId = msg.documentId;
                var doc = _documents[documentId];

                var sessionId = clientSocket.handshake.sessionID;

                if (!doc)
                {
                    // Perhaps there is a new document pending
                    // If so, ensure that user is the owner of the document
                    var docSessionId = _newDocuments[documentId];
                    
                    if ((docSessionId) && (docSessionId === sessionId))
                    {
                        if (msg.userName)
                        {
                            // User supplied user name in response to previous identify response
                            var user = new User(msg.userName, sessionId);
                            doc = new Document(documentId, user);
                            delete _newDocuments[documentId];
                            _documents[documentId] = doc;
                        }
                        else
                        {
                            // Owner just joined in, send identification request
                            sendMessageToSocket(clientSocket, new JoinDocumentResponse("identify"));
                            return;
                        }                        
                    }
                    else
                    {
                        // If there is no pending or current document, or if the pending document's session
                        // does not match the first request then discard the request
                        sendMessageToSocket(clientSocket, new JoinDocumentResponse("error", null, null, Strings("document_not_found")));
                        return;
                    }                    
                }

                var isOnline = isUserOnline(documentId, sessionId);

                clientSocket.join(documentId);

                var existingUser = doc.getUser(sessionId);

                if (existingUser)
                {
                    // Existing user joined the chat, this can be a user that just came online, or the user
                    // is already online and opened another tab in browser.
                    
                    // If user just came online, send user change event to others
                    if (!isOnline)
                    {
                        // Send online message to other users
                        sendMessageToUsers(documentId, doc.getPeers(existingUser), new UserStatusChangedNotification(documentId, existingUser, "online"));

                        // Send document information back to the user
                        sendMessageToSocket(clientSocket, new JoinDocumentResponse("recognized", doc, sessionId));
                    }
                }
                else
                {    
                    if (msg.userName)
                    {
                        // User is not owner but has provided its identity
                        var user = new User(msg.userName, sessionId);
                        doc.addUser(user);

                        // Send online message to other users
                        sendMessageToPeers(clientSocket, doc, new UserStatusChangedNotification(documentId, user, "online"));                        

                        // Send document information back to the user
                        sendMessageToSocket(clientSocket, new JoinDocumentResponse("recognized", doc, sessionId));  
                    }
                    else
                    {
                        // New user, send identification request
                        sendMessageToSocket(clientSocket, new JoinDocumentResponse("identify"));
                    }
                }
            }

            function documentChanged(clientSocket, msg)
            {
                if (!Validate.paramsNotNull([msg, msg.documentId, msg.changeAction]))
                {
                    return;
                }

                var doc = _documents[msg.documentId];
                if (!doc)
                {
                    sendMessageToSocket(clientSocket, new DocumentChangeResponse(msg.documentId, msg.changeAction.localChangeId, Strings("document_not_found")));
                }

                var changeAction = msg.changeAction;
                var result;                
                var changeId;

                switch(changeAction.id)
                {
                    case TaskCreateAction.id:
                        var localChangeId = msg.changeAction.task.changeId;
                        changeId = doc.createTask(msg.changeAction.predecessorTaskId, msg.changeAction.task);
                        result = new TaskCreateResult(localChangeId, changeId);                        
                    break;

                    case FrameCreateAction.id:
                        var localChangeId = msg.changeAction.frame.changeId;
                        
                        var localShapeChangeIds = [];
                        for (var i = 0; i < msg.changeAction.frame.shapes.length; i++)
                        {
                            localShapeChangeIds.push(msg.changeAction.frame.shapes[i].changeId);
                        }

                        var frame = doc.createFrame(msg.changeAction.taskChangeId, msg.changeAction.frame, msg.changeAction.predecessorFrameId);
                        changeId = frame.changeId;

                        var shapeChangeIds = {};
                        for (var i = 0; i < localShapeChangeIds.length; i++)
                        {
                            shapeChangeIds[localShapeChangeIds[i]] = msg.changeAction.frame.shapes[i].changeId;
                        }

                        result = new FrameCreateResult(msg.changeAction.taskChangeId, localChangeId, changeId, shapeChangeIds);
                    break;

                    case FrameDeleteAction.id:
                        doc.deleteFrame(msg.changeAction.taskChangeId, msg.changeAction.frameChangeId);
                        result = true;
                    break;

                    case ElementCreateAction.id:
                        var localChangeId = msg.changeAction.element.changeId;
                        changeId = doc.createElement(msg.changeAction.taskChangeId, msg.changeAction.frameChangeId, msg.changeAction.element);                       
                        result = new ElementCreateResult(localChangeId, changeId);
                    break;

                    case ElementDeleteAction.id:
                        doc.deleteElements(msg.changeAction.taskChangeId, msg.changeAction.frameChangeId, msg.changeAction.changeIds);
                        result = true;
                    break;

                    case ElementModifyAction.id:
                        doc.modifyElements(msg.changeAction.taskChangeId, msg.changeAction.frameChangeId, msg.changeAction.elements);
                        result = true;
                    break;

                    case FrameReorderAction.id:
                        doc.reorderFrame(msg.changeAction.taskChangeId, msg.changeAction.frameChangeId, msg.changeAction.newIndex);
                        result = true;
                    break;

                    case AddSoundAction.id:
                        doc.addSound(msg.changeAction.soundUrl);
                        result = true;
                    break;

                    default:
                        return;
                    break;
                }

                // Send response to client
                sendMessageToSocket(clientSocket, new DocumentChangeResponse(msg.documentId, result));

                // Notify about the change to all others
                var changeNotification = new DocumentChangedNotification(msg.documentId, changeId, msg.changeAction);
                sendMessageToPeers(clientSocket, doc, changeNotification);                
            }

            function socketDisconnected(socket)
            {
                var documents = io.sockets.manager.roomClients[socket.id];

                for (var index in documents)
                {
                    var documentId = index.replace('/', '');
                    var doc = _documents[documentId];
                    if (!doc)
                    {
                        continue;
                    }

                    var msg = new UserStatusChangedNotification(doc.getDocumentId(), doc.getUser(socket.handshake.sessionID), "offline");
                    socket.broadcast.to(documentId).emit(msg.id, msg);

                    socket.leave(documentId);
                }
            }

            function filterSocketsForDocument(documentId, filterFunc)
            {
                var socketIds = io.sockets.manager.rooms["/" + documentId];

                var sockets = [];
                
                if (socketIds)
                {
                    for (i = 0; i < socketIds.length; i++)
                    {
                        var socket = io.sockets.sockets[socketIds[i]];
                        if ((filterFunc) && (filterFunc(socket)))
                        {
                            sockets.push(socket);
                        }
                    }
                }

                return sockets;
            }

            function isUserOnline(documentId, sessionId)
            {
                var sockets = filterSocketsForDocument(documentId, function(socket)
                {
                    return (socket.handshake.sessionID === sessionId);
                });

                return ((sockets) && (sockets.length > 0));
            }

            function sendMessageToPeers(socket, doc, msg)
            {
                var user = doc.getUser(socket.handshake.sessionID);

                if (user)
                {
                    sendMessageToUsers(doc.getDocumentId(), doc.getPeers(user), msg);
                }
            }

            function sendMessageToUsers(documentId, users, msg)
            {
                var sessions = {};
                for (i = 0; i < users.length; i++)
                {
                    sessions[users[i].getSessionId()] = 1;
                }

                var sockets = filterSocketsForDocument(documentId, function(socket)
                {
                    return sessions[socket.handshake.sessionID];
                });

                sendMessageToSockets(sockets, msg);
            }

            function sendMessageToSocket(socket, msg)
            {
                var sockets = [socket];
                sendMessageToSockets(sockets, msg);
            }

            function sendMessageToSockets(sockets, msg)
            {
                for (i = 0; i < sockets.length; i++)
                {
                    sockets[i].emit(msg.id, msg);
                }
            }

            function bindEvent(event, socket, func)
            {
                socket.on(event, function(data) {func(socket, data);});
            }
        };

        var _instance;

        return function()
        {
            this.getInstance = function(io)
            {
                if (!_instance)
                {
                    _instance = new singleton(io);
                }
                return _instance;
            };
        };        
    }
);